/*
 * Copyright 2013 JROSE
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jrose.core.config;

import lombok.Cleanup;
import lombok.NoArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jrose.api.config.CommonConfig;
import org.jrose.api.config.ConfigManager;
import org.jrose.api.config.DataSourceConfig;
import org.jrose.api.config.MemcachedConfig;
import org.jrose.api.config.MongoConfig;
import org.jrose.core.runtime.CurrentRuntimeLevel;
import org.jrose.core.runtime.RuntimeLevel;
import org.jrose.core.util.CollectionUtils;
import org.jrose.core.util.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Default {@link ConfigManager} implementation.
 *
 * @author Xiaohai Zhang
 * @since 2013-08-26 14:11
 */
@NoArgsConstructor
public class ConfigManagerImpl implements ConfigManager {
    protected final Logger log = LoggerFactory.getLogger("config.manager");

    private static final String DEFAULT_RESOURCE_PATH = "/config/config.json";

    private static final MutableObject<ConfigManager> delegate = new MutableObject<>(null);

    static {
        if (CurrentRuntimeLevel.le(RuntimeLevel.UNIT_TEST)) {
            String mock = "org.jrose.test.config.MockConfigManager";
            try {
                Class<?> mockClass = ConfigManagerImpl.class.getClassLoader().loadClass(mock);
                delegate.setValue((ConfigManager) mockClass.newInstance());
            } catch (Exception ex) {
                throw new RuntimeException(ex.getMessage(), ex);
            }
        }
    }

    private final Map<String, DataSourceConfig> dataSourceConfigs = new HashMap<>();
    private final MutableObject<MongoConfig> mongoConfig = new MutableObject<>(null);
    private final MutableObject<MemcachedConfig> memcachedConfig = new MutableObject<>(null);
    private final MutableObject<CommonConfig> commonConfig = new MutableObject<>(new CommonConfig());
    private final AtomicBoolean loaded = new AtomicBoolean(false);

    @Override
    public DataSourceConfig getDataSourceConfig(String name) {
        if (delegate.getValue() != null) {
            return delegate.getValue().getDataSourceConfig(name);
        }
        loadResourceIfNecessary();
        return dataSourceConfigs.get(name);
    }

    @Override
    public MongoConfig getMongoConfig() {
        if (delegate.getValue() != null) {
            return delegate.getValue().getMongoConfig();
        }
        loadResourceIfNecessary();
        return mongoConfig.getValue();
    }

    @Override
    public MemcachedConfig getMemcachedConfig() {
        loadResourceIfNecessary();
        return memcachedConfig.getValue();
    }

    @Override
    public CommonConfig getCommonConfig() {
        loadResourceIfNecessary();
        return commonConfig.getValue();
    }

    private synchronized void loadResourceIfNecessary() {
        if (loaded.get()) {
            return;
        }
        try {
            Resource resource = new ClassPathResource(DEFAULT_RESOURCE_PATH);
            Validate.isTrue(resource.exists(), "No '%s' found in classpath", DEFAULT_RESOURCE_PATH);

            Map<String, Object> content = doLoadResource(resource);
            Map<String, Map> dataSourceConfigs = mergeConfigs((Map) content.get("dataSourceConfig"));
            for (String name : dataSourceConfigs.keySet()) {
                Map value = dataSourceConfigs.get(name);
                this.dataSourceConfigs.put(name, JsonUtils.fromJson(JsonUtils.toJson(value), DataSourceConfig.class));
                log.debug("Loaded data source configuration '{}'", name);
            }

            this.mongoConfig.setValue(JsonUtils.fromJson(JsonUtils.toJson(content.get("mongoConfig")), MongoConfig.class));
            log.debug("Loaded mongo configuration");

            this.memcachedConfig.setValue(JsonUtils.fromJson(JsonUtils.toJson(content.get("memcachedConfig")), MemcachedConfig.class));
            log.debug("Loaded memcached configuration");

            this.commonConfig.getValue().putAll(JsonUtils.fromJson(JsonUtils.toJson(content.get("commonConfig"))));
            log.debug("Loaded common configuration");
        } catch (Exception ex) {
            if (ex instanceof RuntimeException) {
                throw (RuntimeException) ex;
            }
            throw new RuntimeException(ex.getMessage(), ex);
        }
        loaded.set(true);
    }

    protected Map<String, Object> doLoadResource(Resource resource) throws Exception {
        @Cleanup InputStream inStream = resource.getInputStream();
        String content = IOUtils.toString(inStream, "UTF-8");
        return JsonUtils.fromJson(content);
    }

    private Map<String, Map> mergeConfigs(Map source) {
        if (CollectionUtils.isEmpty(source)) {
            return Collections.emptyMap();
        }
        Map<String, Map> result = new LinkedHashMap<>();
        Map defaultMap = (Map) source.get("_default");
        for (Object each : source.keySet()) {
            String name = (String) each;
            if (StringUtils.equals(name, "_default")) {
                continue;
            }
            Map value = (Map) source.get(name);
            Map merged = new LinkedHashMap();
            if (defaultMap != null) {
                //noinspection unchecked
                merged.putAll(defaultMap);
            }
            //noinspection unchecked
            merged.putAll(value);
            result.put(name, merged);
        }
        return result;
    }
}
